1、JavaScript 的数据类型有哪些?
8 种基本数据类型(原始类型)和 1 种引用类型:
- 基本数据类型(Primitive Types):
string
number
boolean
null
undefined
symbol
bigint
- 引用类型(Reference Type):
8.
object
(包括数组、函数等)
2、null
和 undefined
的区别是什么?
null
: 是一个表示“没有值”的特殊对象,通常用于显式地赋值给一个变量,表示这个变量目前没有对象。
undefined
: 表示变量未定义或未赋值,是变量的默认值。
区别:
- 类型不同:
typeof null
是"object"
;typeof undefined
是"undefined"
。
- 语义不同:
null
是一种占位符,表示变量有意地没有值;undefined
则表示未初始化的变量。
3. call
、apply
和 bind
的区别是什么?
this
关键字
在 JavaScript 中,
this
关键字的值取决于它的上下文(也就是它被调用的方式)。call
、apply
和 bind
方法都可以显式地指定函数调用时的 this
值。call
方法
- 用法:
function.call(thisArg, arg1, arg2, ...)
- 功能: 调用一个函数,并显式地设置
this
的值,同时逐个传入参数
- 特点:
call
方法立即调用函数this
的值是第一个参数thisArg
,后续参数作为函数的参数依次传递
//greet 函数通过 call 方法调用时,this 被显式地设置为 person 对象 function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 call 方法,设置 this 为 person 对象,并传递两个参数 greet.call(person, 'Hello', '!'); // 输出: Hello, Alice!
apply
方法
- 用法:
function.apply(thisArg, [argsArray])
- 功能: 调用一个函数,并显式地设置
this
的值,同时以数组形式传入参数。
- 特点:
apply
方法立即调用函数this
的值是第一个参数thisArg
,第二个参数是一个数组,作为函数的参数传递。
function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 apply 方法,设置 this 为 person 对象,并传递参数数组 greet.apply(person, ['Hi', '?']); // 输出: Hi, Alice?
bind
方法
- 用法:
function.bind(thisArg, arg1, arg2, ...)
- 功能: 创建一个新的函数,在调用时强制其
this
的值为指定的对象,同时可以预先传入部分参数。
- 特点:
bind
方法不会立即调用函数,它返回一个新的函数。- 新函数的
this
值是bind
方法的第一个参数thisArg
,并且可以在绑定时传入部分参数。 - 新函数可以像普通函数一样继续传递其他参数。
function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 bind 方法,创建一个新的函数,将 this 绑定为 person 对象,并预设第一个参数 const greetAlice = greet.bind(person, 'Hey'); // 调用新的函数时,只需要传递剩余的参数 greetAlice('!'); // 输出: Hey, Alice!
方法 | 是否立即调用 | 参数形式 | 返回值 | 适用场景 |
call | 是 | 逐个参数传入 | 函数执行结果 | 当需要调用函数,并设置 this 值时使用。 |
apply | 是 | 参数以数组形式传入 | 函数执行结果 | 当需要调用函数,并设置 this 值,且参数以数组形式传递时使用。如对象方法借用或借用构造函数。 |
bind | 否 | 逐个参数传入或部分参数预设 | 绑定了 this 和部分参数的新函数 | 常用于函数需要在稍后调用且希望永久绑定 this 的值的场景,如事件处理器或回调函数。 |
4、let、var、const的区别?
ㅤ | 作用域 | 提升 | 可变性 |
var | 声明的变量具有函数作用域,即它在函数内部声明的变量只在该函数内部可见;如果在函数外部声明,则是全局作用域。 | var 声明的变量会被提升到函数或全局作用域的顶部,但赋值不会被提升。也就是说,可以在声明之前访问变量(但值是 undefined) | var 声明的变量可以被重新赋值,也可以被重新声明 |
let | let 声明的变量具有块级作用域,即它在块级(如 if 语句或循环)内部声明的变量只在该块内有效。 | let 声明的变量会被提升,但不会被初始化。这意味着在声明之前访问该变量会导致 ReferenceError。 | let 声明的变量可以被重新赋值,但不能被重新声明(在同一作用域内)。 |
const | const 声明的变量也具有块级作用域(block scope),与 let 相同。 | const 声明的变量会被提升,但不会被初始化。这意味着在声明之前访问该变量会导致 ReferenceError。 | const 声明的变量必须在声明时初始化,并且一旦赋值后,不能重新赋值(不可变)。但是,对于对象或数组,const 只保证变量绑定不变,对象的内容(如属性或元素)是可以改变的。
|
5、闭包
它指的是一个函数能够访问它外部的变量,即使这个函数在它的外部环境执行。
要理解闭包,我们需要先了解以下几个关键点:
- 词法作用域(Lexical Scope):
JavaScript 使用的是词法作用域规则,这意味着函数的作用域在函数定义时就确定了,而不是在函数调用时。一个函数可以访问它定义时所在的作用域中的变量。
- 函数嵌套:
在 JavaScript 中,我们可以在一个函数内部定义另一个函数。内层函数可以访问外层函数的变量。
- 变量的生存期:
通常情况下,当一个函数执行完毕后,它的局部变量就会被销毁,因为它们的作用域仅限于这个函数内部。然而,如果有其他函数引用了这些局部变量,那么这些变量就会继续存在于内存中。
什么是闭包?
闭包 是指 函数和其定义时的词法环境的组合。也就是说,闭包可以“记住”它创建时所处的环境,即使在闭包被创建的环境之外执行。
🌰例子:
// outerFunction 是一个外部函数,它定义了一个局部变量 outerVariable 和一个内部函数 innerFunction function outerFunction() { let outerVariable = 'I am outside!'; //innerFunction 是一个闭包,因为它访问了外部函数 outerFunction 中的 outerVariable 变量。 function innerFunction() { console.log(outerVariable); } // 当 outerFunction 被调用时,它返回了 innerFunction(没有执行它,而是返回它的引用)。 return innerFunction; } // closure 变量保存了 innerFunction 的引用 const closure = outerFunction(); //当我们调用 closure() 时,实际上调用的是 innerFunction,它依然可以访问 outerVariable,即使 outerFunction 已经执行完毕并返回了。 closure(); // 输出: I am outside!
闭包应用
闭包的主要优势在于它们使得函数能够“记住”它们的词法作用域,即使在作用域链已经发生改变的情况下。闭包可以用来:
- 数据封装:闭包可以隐藏一个函数的实现细节,只暴露想要公开的接口。
//count 变量无法从外部访问,只能通过 increment 函数访问,这就实现了封装。 function counter() { let count = 0; return function() { count++; console.log(count); }; } const increment = counter(); increment(); // 输出: 1 increment(); // 输出: 2 increment(); // 输出: 3
- 模拟私有变量:做函数工厂,创建多个类似的函数,每个函数都拥有自己的私有变量
- 回调函数和事件处理器:在异步编程(例如处理事件或 HTTP 请求)中,闭包非常有用,它们使得回调函数能够访问到在事件触发时所需的变量。
6、原型链
原型(Prototype)是每个JS对象的内部属性,用于实现继承。JS中的所有对象都有一个[[Prototype]](除了一些特殊情况),它要么是另一个对象,要么是 null。
原型链 是多个对象之间的继承关系,它允许一个对象访问另一个对象的属性和方法。如果一个对象的属性或方法在自身找不到,JS 会沿着原型链往上查找,直到找到或到达链的末端(null)。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log('Hello, ' + this.name); }; const john = new Person('John'); john.sayHello(); // Hello, John
在这个例子中,john 对象没有 sayHello 方法,所以它会沿着原型链查找到 Person.prototype,找到 sayHello 并执行。
7、事件循环
事件循环是JS处理异步操作的机制,在执行代码时,JS引擎将同步任务推入调用栈中执行,异步任务则放入任务队列中,任务队列中的任务在调用栈空闲时被放入调用栈执行。
JS是单线程的,同一个时间只能执行一个任务,但它能处理异步任务而不阻塞主线程的执行
调用栈
- 调用栈是一个 LIFO(Last In, First Out)结构,用来管理代码中正在执行的函数。
- 每当一个函数被调用时,它会被推入栈顶,当函数执行完毕后,它会从栈中弹出。
- 如果调用栈中有任务在执行,JavaScript 引擎将无法执行其他代码。
任务队列
任务队列是一个先进先出(FIFO)结构,用于存放已经准备好执行的回调函数。 当调用栈为空时,事件循环会检查任务队列,如果任务队列中有任务,则将其推入调用栈并开始执行
- 微任务:微任务包括 Promise.then 回调、MutationObserver 等。微任务队列的优先级高于宏任务,每当一个宏任务执行完毕后,事件循环会首先清空所有的微任务队列,然后再开始下一个宏任务。
- 宏任务:宏任务包括主代码块(script)、setTimeout、setInterval、I/O 操作等。宏任务执行时,事件循环会依次执行任务队列中的任务。
8、JS性能优化
减少 HTTP 请求
- 合并文件: 将多个 CSS、JavaScript 文件合并成一个文件,以减少 HTTP 请求的次数。
- CSS 雪碧图(Sprites): 将多个小图标合并成一张图片,通过 CSS 控制背景位置来显示不同的图标。
使用内容分发网络(CDN)
- CDN 加速: 使用 CDN 将静态资源(如图片、CSS、JavaScript)分发到多个服务器节点,用户可以从距离最近的节点获取资源,减少加载时间。
资源压缩与缩小
- 文件压缩: 使用 Gzip 或 Brotli 对 HTML、CSS、JavaScript 文件进行压缩,以减少文件体积。
- 代码缩小(Minification): 使用工具(如 Terser、UglifyJS)去除 JavaScript 和 CSS 文件中的空格、注释、无用代码等,进一步减小文件体积。
图片优化
- 图像格式: 使用适当的图片格式,如使用 WebP 替代传统的 JPEG 和 PNG 格式,因为 WebP 体积更小且支持有损和无损压缩。
- 图像压缩: 使用工具(如 ImageOptim、TinyPNG)对图片进行无损或有损压缩,减小图片体积。
- 响应式图片: 根据不同的设备和屏幕大小,提供不同分辨率的图片,以减少不必要的带宽消耗。
延迟加载(Lazy Loading)
- 图片和视频: 对页面中的图片和视频使用延迟加载技术,只有在用户滚动到这些资源可见时才进行加载,以减少初始页面加载时间。
- JavaScript: 对不需要立即执行的 JavaScript 文件使用 async 或 defer 属性,使其在不阻塞 HTML 解析的情况下异步加载。
缓存策略
- 浏览器缓存: 利用浏览器缓存,通过设置适当的缓存头(如 Cache-Control、ETag),减少重复加载相同的资源。
- 服务端缓存: 使用服务端缓存(如 Redis、Varnish)来缓存动态生成的内容,减少服务器的计算压力。
代码分割
- 按需加载: 使用 Webpack 等打包工具进行代码分割,将应用程序分成多个代码块,按需加载用户当前需要的代码,减少初始加载时间。
- 动态导入: 对于大型应用,可以使用动态导入(import())来异步加载模块,进一步优化资源加载。
减少重绘与回流
- 减少 DOM 操作: 批量更新 DOM,避免频繁的 DOM 操作,因为每次 DOM 变化都会导致重绘和回流,影响页面性能。
- CSS 优化: 尽量避免使用对性能影响较大的 CSS 属性(如 float、position: absolute)和复杂的选择器。
预加载与预渲染
- 预加载(Preload): 使用 <link rel="preload"> 标签预加载关键资源,如字体、关键 CSS 和 JavaScript 文件,以确保这些资源在页面渲染前已被加载。
- 预渲染(Prerender): 使用 <link rel="prerender"> 标签提前加载和渲染用户可能访问的页面,以加快用户点击后页面的显示速度。
减少第三方代码的使用
- 优化第三方库: 使用体积更小的第三方库或自行实现一些轻量级的功能,减少加载不必要的第三方代码。
- 异步加载第三方脚本: 对第三方的广告、社交分享、分析代码进行异步加载,避免其影响页面主线程的执行。
性能监控与分析
- 工具使用: 使用 Lighthouse、Chrome DevTools、WebPageTest 等工具对页面性能进行分析,找出性能瓶颈并进行针对性的优化。
- 实时监控: 实施性能监控方案(如 Google Analytics、New Relic),实时监控用户的实际加载时间和交互性能,及时发现和解决问题。